/*
 *   Copyright 2021 zzh
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */

package com.aduib.boot.common.scanner;

import com.aduib.boot.common.exception.CommonException;
import com.aduib.boot.common.log.LOG;
import com.aduib.boot.common.util.ClassUtils;
import com.aduib.boot.common.util.FileUtils;
import com.aduib.boot.common.util.StrUtils;
import com.aduib.boot.common.util.SystemUtils;
import com.aduib.boot.common.util.URLUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * @description: ClassScanner
 * @author: zzh
 * @date: 2021/10/12 16:44
 */
public class ClassPathScanner {

  public static final String JAR = FileUtils.URL_PROTOCOL_JAR;

  public static final String FILE = FileUtils.URL_PROTOCOL_FILE;

  public static final String DOT = StrUtils.DOT;

  public static final String SEPARATOR = StrUtils.SLASH;

  public String fileSuffix = FileUtils.EXT_CLASS;

  private ClassLoader classLoader = null;

  public ClassPathScanner() {}

  public ClassPathScanner(String fileSuffix) {
    this.fileSuffix = fileSuffix;
  }

  public ClassPathScanner(String fileSuffix, ClassLoader classLoader) {
    this.fileSuffix = fileSuffix;
    this.classLoader = classLoader;
  }

  public static ClassPathScanner build() {
    return new ClassPathScanner();
  }

  public static ClassPathScanner build(String fileSuffix) {
    return new ClassPathScanner(fileSuffix);
  }

  public static ClassPathScanner build(String fileSuffix, ClassLoader classLoader) {
    return new ClassPathScanner(fileSuffix);
  }

  public String getFileSuffix() {
    return fileSuffix;
  }

  public void setFileSuffix(String fileSuffix) {
    this.fileSuffix = fileSuffix;
  }

  public ClassLoader getClassLoader() {
    return classLoader;
  }

  public void setClassLoader(ClassLoader classLoader) {
    this.classLoader = classLoader;
  }

  /**
   * 获取内部类路径名
   *
   * @param basePath
   * @return
   */
  private String getInternalClassPath(String basePath) {
    /** 如果基础路径是用点号分隔，则替换为/ 反之不会处理 */
    return basePath.replace(DOT, File.pathSeparator);
  }

  /**
   * 根据基础包路径扫描
   *
   * @param basePackagePath
   * @return
   */
  public List<Resource> getClassNameList(String basePackagePath) {
    return getResourceListByClassPath(basePackagePath).stream()
        .map(x -> new ClassFileResourceAdapter(getInternalClassPath(basePackagePath), x))
        .collect(Collectors.toList());
  }

  /**
   * 根据基础路径扫描
   *
   * @param basePath
   * @return
   */
  public List<Resource> getResourceListByClassPath(String basePath) {
    List<Resource> resourceList = new ArrayList<>();
    ClassLoader exClassLoader =
        (classLoader == null ? ClassUtils.getDefaultClassLoader() : classLoader);
    if (StrUtils.isBlank(basePath)) {
      throw new IllegalArgumentException("have to required basePackagePath");
    }
    LOG.d("start scan basePackagePath: {}, fileSuffix: {} ", basePath, fileSuffix);
    String internalClassPath = getInternalClassPath(basePath);
    try {
      Enumeration<URL> classPaths = exClassLoader.getResources(internalClassPath);
      while (classPaths.hasMoreElements()) {
        URL classPathUrl = classPaths.nextElement();
        LOG.d("search resource file on baseClassPath: {}", classPathUrl);
        if (classPathUrl.getProtocol().equals(FILE)) {
          List<Resource> fileResourceList = getResourceListByFile(classPathUrl, fileSuffix);
          for (Resource resource : fileResourceList) {
            LOG.t("found resource file with path: {}", resource.getName());
            resourceList.add(resource);
          }
        }

        if (classPathUrl.getProtocol().equals(JAR)) {
          JarURLConnection urlConnection = (JarURLConnection) classPathUrl.openConnection();
          JarFile jarFile = urlConnection.getJarFile();
          List<Resource> jarResource = getResourceListByJar(jarFile, internalClassPath, fileSuffix);
          for (Resource resource : jarResource) {
            LOG.t("found resource file with path: {}", resource.getName());
            resourceList.add(resource);
          }
        }
      }
    } catch (IOException e) {
      throw new CommonException("scan classPath resources have exception: " + e);
    }
    return resourceList;
  }

  /**
   * 获取文件资源列表
   *
   * @param basePath
   * @return
   */
  private List<Resource> getResourceListByFile(URL basePath, String fileSuffix) {
    Path path = Paths.get(URLUtils.toURI(basePath));
    List<File> files =
        FileUtils.loopFiles(
            path, -1, file -> StrUtils.isBlank(fileSuffix) || file.getName().endsWith(fileSuffix));
    return files.stream().map(FileSystemResource::new).collect(Collectors.toList());
  }

  /**
   * 获取文件资源列表
   *
   * @param basePath
   * @param fileSuffix
   * @param jarFile
   * @return
   */
  private List<Resource> getResourceListByJar(JarFile jarFile, String basePath, String fileSuffix) {
    final List<Resource> classFilePaths = new ArrayList<>();
    Enumeration<JarEntry> entries = jarFile.entries();
    while (entries.hasMoreElements()) {
      JarEntry jarEntry = entries.nextElement();
      String name = jarEntry.getName();
      if (!jarEntry.isDirectory()
          && (StrUtils.isBlank(fileSuffix) || name.endsWith(fileSuffix))
          && name.contains(basePath)) {
        classFilePaths.add(new zipEntryResource(jarFile, jarEntry));
      }
    }
    return classFilePaths;
  }

  public interface Resource {

    InputStream getInputStream();

    default ReadableByteChannel getReadableChannel() {
      return Channels.newChannel(getInputStream());
    }

    String getName();

    default Resource getResource() {
      return this;
    }
  }

  static class FileSystemResource implements Resource {

    private File file;

    public FileSystemResource(File file) {
      this.file = file;
    }

    @Override
    public InputStream getInputStream() {
      try {
        return new FileInputStream(file);
      } catch (FileNotFoundException e) {
        throw new CommonException("open file inputStream fail and ex: ", e);
      }
    }

    @Override
    public ReadableByteChannel getReadableChannel() {
      return Channels.newChannel(getInputStream());
    }

    /**
     * 文件名作为资源名
     *
     * @return
     */
    @Override
    public String getName() {
      if (SystemUtils.INSTANCE.isWindows()) {
        //        String absolutePath = file.getAbsolutePath();
        String absolutePath = file.getPath();
        if (absolutePath.startsWith(SEPARATOR)) {
          absolutePath =
              absolutePath
                  .substring(absolutePath.indexOf(SEPARATOR))
                  .replace(File.separator, SEPARATOR);
        }
        return absolutePath.replace(File.separator, SEPARATOR);
      }
      return file.getPath();
    }

    @Override
    public String toString() {
      return "resource{" + "resourceName=" + getName() + '}';
    }
  }
  /** 压缩包资源 */
  static class zipEntryResource implements Resource {

    private ZipFile zipFile;

    private ZipEntry zipEntry;

    public zipEntryResource(ZipFile zipFile, ZipEntry zipEntry) {
      this.zipFile = zipFile;
      this.zipEntry = zipEntry;
    }

    @Override
    public InputStream getInputStream() {
      try {
        return zipFile.getInputStream(zipEntry);
      } catch (IOException e) {
        throw new CommonException("open file inputStream fail and ex: ", e);
      }
    }

    @Override
    public ReadableByteChannel getReadableChannel() {
      return Channels.newChannel(getInputStream());
    }

    /**
     * 压缩包内文件路径名作为资源名
     *
     * @return
     */
    @Override
    public String getName() {
      return zipEntry.getName();
    }

    @Override
    public String toString() {
      return "resource{" + "resourceName=" + getName() + '}';
    }
  }

  static class ClassFileResourceAdapter implements Resource {

    private String basePackagePath;

    private Resource resource;

    public ClassFileResourceAdapter(String basePackagePath, Resource resource) {
      this.basePackagePath = basePackagePath;
      this.resource = resource;
    }

    public static Resource build(Resource resource, String basePackagePath) {
      return new ClassFileResourceAdapter(basePackagePath, resource);
    }

    @Override
    public Resource getResource() {
      return resource;
    }

    @Override
    public InputStream getInputStream() {
      return resource.getInputStream();
    }

    @Override
    public String getName() {
      if (resource instanceof zipEntryResource) {
        return resource
                .getName()
                .replace(SEPARATOR, DOT)
                .substring(0, resource.getName().lastIndexOf(DOT));
      } else if (resource instanceof FileSystemResource) {
        String classFilePath = resource.getName();
        //        String internalPath =
        // basePackagePath.contains(SEPARATOR)?basePackagePath:basePackagePath.replace(SEPARATOR,
        // File.separator);
        return classFilePath
                .substring(classFilePath.indexOf(basePackagePath), classFilePath.lastIndexOf(DOT))
                .replace(SEPARATOR, DOT);
      }
      return "";
    }

    @Override
    public String toString() {
      return "resource{"
          + "basePackagePath='"
          + basePackagePath
          + '\''
          + ", resourceName="
          + resource.getName()
          + '}';
    }
  }
}
